// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/message_center/views/message_center_view.h"

#include <list>
#include <map>

#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/message_loop/message_loop.h"
#include "base/stl_util.h"
#include "build/build_config.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/animation/multi_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/message_center_style.h"
#include "ui/message_center/message_center_tray.h"
#include "ui/message_center/message_center_types.h"
#include "ui/message_center/views/message_center_button_bar.h"
#include "ui/message_center/views/message_list_view.h"
#include "ui/message_center/views/message_view.h"
#include "ui/message_center/views/message_view_context_menu_controller.h"
#include "ui/message_center/views/notification_view.h"
#include "ui/message_center/views/notifier_settings_view.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/scrollbar/overlay_scroll_bar.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/widget/widget.h"

namespace message_center {

namespace {

    const SkColor kNoNotificationsTextColor = SkColorSetRGB(0xb4, 0xb4, 0xb4);
#if defined(OS_LINUX) && defined(OS_CHROMEOS)
    const SkColor kTransparentColor = SkColorSetARGB(0, 0, 0, 0);
#endif

    const int kDefaultAnimationDurationMs = 120;
    const int kDefaultFrameRateHz = 60;

    void SetViewHierarchyEnabled(views::View* view, bool enabled)
    {
        for (int i = 0; i < view->child_count(); i++)
            SetViewHierarchyEnabled(view->child_at(i), enabled);
        view->SetEnabled(enabled);
    }

} // namespace

class NoNotificationMessageView : public views::View {
public:
    NoNotificationMessageView();
    ~NoNotificationMessageView() override;

    // Overridden from views::View.
    gfx::Size GetPreferredSize() const override;
    int GetHeightForWidth(int width) const override;
    void Layout() override;

private:
    views::Label* label_;

    DISALLOW_COPY_AND_ASSIGN(NoNotificationMessageView);
};

NoNotificationMessageView::NoNotificationMessageView()
{
    label_ = new views::Label(l10n_util::GetStringUTF16(
        IDS_MESSAGE_CENTER_NO_MESSAGES));
    label_->SetAutoColorReadabilityEnabled(false);
    label_->SetEnabledColor(kNoNotificationsTextColor);
    // Set transparent background to ensure that subpixel rendering
    // is disabled. See crbug.com/169056
#if defined(OS_LINUX) && defined(OS_CHROMEOS)
    label_->SetBackgroundColor(kTransparentColor);
#endif
    AddChildView(label_);
}

NoNotificationMessageView::~NoNotificationMessageView()
{
}

gfx::Size NoNotificationMessageView::GetPreferredSize() const
{
    return gfx::Size(kMinScrollViewHeight, label_->GetPreferredSize().width());
}

int NoNotificationMessageView::GetHeightForWidth(int width) const
{
    return kMinScrollViewHeight;
}

void NoNotificationMessageView::Layout()
{
    int text_height = label_->GetHeightForWidth(width());
    int margin = (height() - text_height) / 2;
    label_->SetBounds(0, margin, width(), text_height);
}

// MessageCenterView ///////////////////////////////////////////////////////////

MessageCenterView::MessageCenterView(MessageCenter* message_center,
    MessageCenterTray* tray,
    int max_height,
    bool initially_settings_visible,
    bool top_down,
    const base::string16& title)
    : message_center_(message_center)
    , tray_(tray)
    , scroller_(NULL)
    , settings_view_(NULL)
    , button_bar_(NULL)
    , top_down_(top_down)
    , settings_visible_(initially_settings_visible)
    , source_view_(NULL)
    , source_height_(0)
    , target_view_(NULL)
    , target_height_(0)
    , is_closing_(false)
    , context_menu_controller_(new MessageViewContextMenuController(this))
{
    message_center_->AddObserver(this);
    set_notify_enter_exit_on_child(true);
    set_background(views::Background::CreateSolidBackground(
        kMessageCenterBackgroundColor));

    NotifierSettingsProvider* notifier_settings_provider = message_center_->GetNotifierSettingsProvider();
    button_bar_ = new MessageCenterButtonBar(this,
        message_center,
        notifier_settings_provider,
        initially_settings_visible,
        title);

    const int button_height = button_bar_->GetPreferredSize().height();

    scroller_ = new views::ScrollView();
    scroller_->ClipHeightTo(kMinScrollViewHeight, max_height - button_height);
    scroller_->SetVerticalScrollBar(new views::OverlayScrollBar(false));
    scroller_->set_background(
        views::Background::CreateSolidBackground(kMessageCenterBackgroundColor));

    scroller_->SetPaintToLayer(true);
    scroller_->SetFillsBoundsOpaquely(false);
    scroller_->layer()->SetMasksToBounds(true);

    empty_list_view_.reset(new NoNotificationMessageView);
    empty_list_view_->set_owned_by_client();
    message_list_view_.reset(new MessageListView(this, top_down));
    message_list_view_->set_owned_by_client();

    // We want to swap the contents of the scroll view between the empty list
    // view and the message list view, without constructing them afresh each
    // time.  So, since the scroll view deletes old contents each time you
    // set the contents (regardless of the |owned_by_client_| setting) we need
    // an intermediate view for the contents whose children we can swap in and
    // out.
    views::View* scroller_contents = new views::View();
    scroller_contents->SetLayoutManager(new views::FillLayout());
    scroller_contents->AddChildView(empty_list_view_.get());
    scroller_->SetContents(scroller_contents);

    settings_view_ = new NotifierSettingsView(notifier_settings_provider);

    if (initially_settings_visible)
        scroller_->SetVisible(false);
    else
        settings_view_->SetVisible(false);

    AddChildView(scroller_);
    AddChildView(settings_view_);
    AddChildView(button_bar_);
}

MessageCenterView::~MessageCenterView()
{
    if (!is_closing_)
        message_center_->RemoveObserver(this);
}

void MessageCenterView::SetNotifications(
    const NotificationList::Notifications& notifications)
{
    if (is_closing_)
        return;

    notification_views_.clear();

    int index = 0;
    for (NotificationList::Notifications::const_iterator iter = notifications.begin(); iter != notifications.end(); ++iter) {
        AddNotificationAt(*(*iter), index++);

        message_center_->DisplayedNotification(
            (*iter)->id(), message_center::DISPLAY_SOURCE_MESSAGE_CENTER);
        if (notification_views_.size() >= kMaxVisibleMessageCenterNotifications)
            break;
    }

    NotificationsChanged();
    scroller_->RequestFocus();
}

void MessageCenterView::SetSettingsVisible(bool visible)
{
    if (is_closing_)
        return;

    if (visible == settings_visible_)
        return;

    settings_visible_ = visible;

    if (visible) {
        source_view_ = scroller_;
        target_view_ = settings_view_;
    } else {
        source_view_ = settings_view_;
        target_view_ = scroller_;
    }
    source_height_ = source_view_->GetHeightForWidth(width());
    target_height_ = target_view_->GetHeightForWidth(width());

    gfx::MultiAnimation::Parts parts;
    // First part: slide resize animation.
    parts.push_back(gfx::MultiAnimation::Part(
        (source_height_ == target_height_) ? 0 : kDefaultAnimationDurationMs,
        gfx::Tween::EASE_OUT));
    // Second part: fade-out the source_view.
    if (source_view_->layer()) {
        parts.push_back(gfx::MultiAnimation::Part(
            kDefaultAnimationDurationMs, gfx::Tween::LINEAR));
    } else {
        parts.push_back(gfx::MultiAnimation::Part());
    }
    // Third part: fade-in the target_view.
    if (target_view_->layer()) {
        parts.push_back(gfx::MultiAnimation::Part(
            kDefaultAnimationDurationMs, gfx::Tween::LINEAR));
        target_view_->layer()->SetOpacity(0);
        target_view_->SetVisible(true);
    } else {
        parts.push_back(gfx::MultiAnimation::Part());
    }
    settings_transition_animation_.reset(new gfx::MultiAnimation(
        parts, base::TimeDelta::FromMicroseconds(1000000 / kDefaultFrameRateHz)));
    settings_transition_animation_->set_delegate(this);
    settings_transition_animation_->set_continuous(false);
    settings_transition_animation_->Start();

    button_bar_->SetBackArrowVisible(visible);
}

void MessageCenterView::ClearAllNotifications()
{
    if (is_closing_)
        return;

    SetViewHierarchyEnabled(scroller_, false);
    button_bar_->SetAllButtonsEnabled(false);
    message_list_view_->ClearAllNotifications(scroller_->GetVisibleRect());
}

void MessageCenterView::OnAllNotificationsCleared()
{
    SetViewHierarchyEnabled(scroller_, true);
    button_bar_->SetAllButtonsEnabled(true);
    button_bar_->SetCloseAllButtonEnabled(false);
    message_center_->RemoveAllVisibleNotifications(true); // Action by user.
}

size_t MessageCenterView::NumMessageViewsForTest() const
{
    return message_list_view_->child_count();
}

void MessageCenterView::OnSettingsChanged()
{
    scroller_->InvalidateLayout();
    PreferredSizeChanged();
    Layout();
}

void MessageCenterView::SetIsClosing(bool is_closing)
{
    is_closing_ = is_closing;
    if (is_closing)
        message_center_->RemoveObserver(this);
    else
        message_center_->AddObserver(this);
}

void MessageCenterView::Layout()
{
    if (is_closing_)
        return;

    int button_height = button_bar_->GetHeightForWidth(width()) + button_bar_->GetInsets().height();
    // Skip unnecessary re-layout of contents during the resize animation.
    bool animating = settings_transition_animation_ && settings_transition_animation_->is_animating();
    if (animating && settings_transition_animation_->current_part_index() == 0) {
        if (!top_down_) {
            button_bar_->SetBounds(
                0, height() - button_height, width(), button_height);
        }
        return;
    }

    scroller_->SetBounds(0,
        top_down_ ? button_height : 0,
        width(),
        height() - button_height);
    settings_view_->SetBounds(0,
        top_down_ ? button_height : 0,
        width(),
        height() - button_height);

    bool is_scrollable = false;
    if (scroller_->visible())
        is_scrollable = scroller_->height() < message_list_view_->height();
    else
        is_scrollable = settings_view_->IsScrollable();

    if (!animating) {
        if (is_scrollable) {
            // Draw separator line on the top of the button bar if it is on the bottom
            // or draw it at the bottom if the bar is on the top.
            button_bar_->SetBorder(views::Border::CreateSolidSidedBorder(
                top_down_ ? 0 : 1, 0, top_down_ ? 1 : 0, 0, kFooterDelimiterColor));
        } else {
            button_bar_->SetBorder(views::Border::CreateEmptyBorder(
                top_down_ ? 0 : 1, 0, top_down_ ? 1 : 0, 0));
        }
        button_bar_->SchedulePaint();
    }
    button_bar_->SetBounds(0,
        top_down_ ? 0 : height() - button_height,
        width(),
        button_height);
    if (GetWidget())
        GetWidget()->GetRootView()->SchedulePaint();
}

gfx::Size MessageCenterView::GetPreferredSize() const
{
    if (settings_transition_animation_ && settings_transition_animation_->is_animating()) {
        int content_width = std::max(source_view_->GetPreferredSize().width(),
            target_view_->GetPreferredSize().width());
        int width = std::max(content_width,
            button_bar_->GetPreferredSize().width());
        return gfx::Size(width, GetHeightForWidth(width));
    }

    int width = 0;
    for (int i = 0; i < child_count(); ++i) {
        const views::View* child = child_at(0);
        if (child->visible())
            width = std::max(width, child->GetPreferredSize().width());
    }
    return gfx::Size(width, GetHeightForWidth(width));
}

int MessageCenterView::GetHeightForWidth(int width) const
{
    if (settings_transition_animation_ && settings_transition_animation_->is_animating()) {
        int content_height = target_height_;
        if (settings_transition_animation_->current_part_index() == 0) {
            content_height = settings_transition_animation_->CurrentValueBetween(
                source_height_, target_height_);
        }
        return button_bar_->GetHeightForWidth(width) + content_height;
    }

    int content_height = 0;
    if (scroller_->visible())
        content_height += scroller_->GetHeightForWidth(width);
    else
        content_height += settings_view_->GetHeightForWidth(width);
    return button_bar_->GetHeightForWidth(width) + button_bar_->GetInsets().height() + content_height;
}

bool MessageCenterView::OnMouseWheel(const ui::MouseWheelEvent& event)
{
    // Do not rely on the default scroll event handler of ScrollView because
    // the scroll happens only when the focus is on the ScrollView. The
    // notification center will allow the scrolling even when the focus is on
    // the buttons.
    if (scroller_->bounds().Contains(event.location()))
        return scroller_->OnMouseWheel(event);
    return views::View::OnMouseWheel(event);
}

void MessageCenterView::OnMouseExited(const ui::MouseEvent& event)
{
    if (is_closing_)
        return;

    message_list_view_->ResetRepositionSession();
    NotificationsChanged();
}

void MessageCenterView::OnNotificationAdded(const std::string& id)
{
    int index = 0;
    const NotificationList::Notifications& notifications = message_center_->GetVisibleNotifications();
    for (NotificationList::Notifications::const_iterator iter = notifications.begin(); iter != notifications.end();
         ++iter, ++index) {
        if ((*iter)->id() == id) {
            AddNotificationAt(*(*iter), index);
            break;
        }
        if (notification_views_.size() >= kMaxVisibleMessageCenterNotifications)
            break;
    }
    NotificationsChanged();
}

void MessageCenterView::OnNotificationRemoved(const std::string& id,
    bool by_user)
{
    NotificationViewsMap::iterator view_iter = notification_views_.find(id);
    if (view_iter == notification_views_.end())
        return;
    NotificationView* view = view_iter->second;
    int index = message_list_view_->GetIndexOf(view);
    DCHECK_LE(0, index);
    if (by_user) {
        message_list_view_->SetRepositionTarget(view->bounds());
        // Moves the keyboard focus to the next notification if the removed
        // notification is focused so that the user can dismiss notifications
        // without re-focusing by tab key.
        if (view->IsCloseButtonFocused() || view == GetFocusManager()->GetFocusedView()) {
            views::View* next_focused_view = NULL;
            if (message_list_view_->child_count() > index + 1)
                next_focused_view = message_list_view_->child_at(index + 1);
            else if (index > 0)
                next_focused_view = message_list_view_->child_at(index - 1);

            if (next_focused_view) {
                if (view->IsCloseButtonFocused()) {
                    // Safe cast since all views in MessageListView are MessageViews.
                    static_cast<MessageView*>(
                        next_focused_view)
                        ->RequestFocusOnCloseButton();
                } else {
                    next_focused_view->RequestFocus();
                }
            }
        }
    }
    message_list_view_->RemoveNotification(view);
    notification_views_.erase(view_iter);
    NotificationsChanged();
}

void MessageCenterView::OnNotificationUpdated(const std::string& id)
{
    NotificationViewsMap::const_iterator view_iter = notification_views_.find(id);
    if (view_iter == notification_views_.end())
        return;

    // Set the item on the mouse cursor as the reposition target so that it
    // should stick to the current position over the update.
    bool set = false;
    if (message_list_view_->IsMouseHovered()) {
        for (const auto& hover_id_view : notification_views_) {
            NotificationView* hover_view = hover_id_view.second;
            if (hover_view->IsMouseHovered()) {
                message_list_view_->SetRepositionTarget(hover_view->bounds());
                set = true;
                break;
            }
        }
    }
    if (!set)
        message_list_view_->ResetRepositionSession();

    // TODO(dimich): add MessageCenter::GetVisibleNotificationById(id)
    NotificationView* view = view_iter->second;
    const NotificationList::Notifications& notifications = message_center_->GetVisibleNotifications();
    for (NotificationList::Notifications::const_iterator iter = notifications.begin(); iter != notifications.end(); ++iter) {
        if ((*iter)->id() == id) {
            int old_width = view->width();
            int old_height = view->GetHeightForWidth(old_width);
            message_list_view_->UpdateNotification(view, **iter);
            if (view->GetHeightForWidth(old_width) != old_height)
                NotificationsChanged();
            break;
        }
    }
}

void MessageCenterView::ClickOnNotification(
    const std::string& notification_id)
{
    message_center_->ClickOnNotification(notification_id);
}

void MessageCenterView::RemoveNotification(const std::string& notification_id,
    bool by_user)
{
    message_center_->RemoveNotification(notification_id, by_user);
}

scoped_ptr<ui::MenuModel> MessageCenterView::CreateMenuModel(
    const NotifierId& notifier_id,
    const base::string16& display_source)
{
    return tray_->CreateNotificationMenuModel(notifier_id, display_source);
}

bool MessageCenterView::HasClickedListener(const std::string& notification_id)
{
    return message_center_->HasClickedListener(notification_id);
}

void MessageCenterView::ClickOnNotificationButton(
    const std::string& notification_id,
    int button_index)
{
    message_center_->ClickOnNotificationButton(notification_id, button_index);
}

void MessageCenterView::ClickOnSettingsButton(
    const std::string& notification_id)
{
    message_center_->ClickOnSettingsButton(notification_id);
}

void MessageCenterView::AnimationEnded(const gfx::Animation* animation)
{
    DCHECK_EQ(animation, settings_transition_animation_.get());

    Visibility visibility = target_view_ == settings_view_
        ? VISIBILITY_SETTINGS
        : VISIBILITY_MESSAGE_CENTER;
    message_center_->SetVisibility(visibility);

    source_view_->SetVisible(false);
    target_view_->SetVisible(true);
    if (source_view_->layer())
        source_view_->layer()->SetOpacity(1.0);
    if (target_view_->layer())
        target_view_->layer()->SetOpacity(1.0);
    settings_transition_animation_.reset();
    PreferredSizeChanged();
    Layout();
}

void MessageCenterView::AnimationProgressed(const gfx::Animation* animation)
{
    DCHECK_EQ(animation, settings_transition_animation_.get());
    PreferredSizeChanged();
    if (settings_transition_animation_->current_part_index() == 1 && source_view_->layer()) {
        source_view_->layer()->SetOpacity(
            1.0 - settings_transition_animation_->GetCurrentValue());
        SchedulePaint();
    } else if (settings_transition_animation_->current_part_index() == 2 && target_view_->layer()) {
        target_view_->layer()->SetOpacity(
            settings_transition_animation_->GetCurrentValue());
        SchedulePaint();
    }
}

void MessageCenterView::AnimationCanceled(const gfx::Animation* animation)
{
    DCHECK_EQ(animation, settings_transition_animation_.get());
    AnimationEnded(animation);
}

void MessageCenterView::AddNotificationAt(const Notification& notification,
    int index)
{
    NotificationView* view = NotificationView::Create(this, notification, false); // Not top-level.
    view->set_context_menu_controller(context_menu_controller_.get());
    notification_views_[notification.id()] = view;
    view->set_scroller(scroller_);
    message_list_view_->AddNotificationAt(view, index);
}

void MessageCenterView::NotificationsChanged()
{
    bool no_message_views = notification_views_.empty();

    // When the child view is removed from the hierarchy, its focus is cleared.
    // In this case we want to save which view has focus so that the user can
    // continue to interact with notifications in the order they were expecting.
    views::FocusManager* focus_manager = scroller_->GetFocusManager();
    View* focused_view = NULL;
    // |focus_manager| can be NULL in tests.
    if (focus_manager)
        focused_view = focus_manager->GetFocusedView();

    // All the children of this view are owned by |this|.
    scroller_->contents()->RemoveAllChildViews(/*delete_children=*/false);
    scroller_->contents()->AddChildView(
        no_message_views ? empty_list_view_.get() : message_list_view_.get());

    button_bar_->SetCloseAllButtonEnabled(!no_message_views);
    scroller_->SetFocusable(!no_message_views);

    if (focus_manager && focused_view)
        focus_manager->SetFocusedView(focused_view);

    scroller_->InvalidateLayout();
    PreferredSizeChanged();
    Layout();
}

void MessageCenterView::SetNotificationViewForTest(MessageView* view)
{
    message_list_view_->AddNotificationAt(view, 0);
}

} // namespace message_center
